const b64chars: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
const b64tab: Record<string, number> = {}
for (let i = 0, l = b64chars.length; i < l; i++) b64tab[b64chars.charAt(i)] = i

const fromCharCode = String.fromCharCode
// encoder stuff
const cb_utob = function (c: string): string {
  if (c.length < 2) {
    const cc = c.charCodeAt(0)
    return cc < 0x80 ? c
      : cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6)) + fromCharCode(0x80 | (cc & 0x3f)))
        : (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) +
          fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
          fromCharCode(0x80 | (cc & 0x3f)))
  } else {
    const cc = 0x10000 + (c.charCodeAt(0) - 0xD800) * 0x400 + (c.charCodeAt(1) - 0xDC00)
    return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) +
      fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) +
      fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
      fromCharCode(0x80 | (cc & 0x3f)))
  }
}

const re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g
const utob = function (u: string) {
  return u.replace(re_utob, cb_utob)
}

const cb_encode = function (ccc: string) {
  const padLen = [0, 2, 1][ccc.length % 3]
  const ord = ccc.charCodeAt(0) << 16 |
    ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) |
    ((ccc.length > 2 ? ccc.charCodeAt(2) : 0))
  const chars = [
    b64chars.charAt(ord >>> 18),
    b64chars.charAt((ord >>> 12) & 63),
    padLen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
    padLen >= 1 ? '=' : b64chars.charAt(ord & 63)
  ]
  return chars.join('')
}

const btoa = (b: string) => b.replace(/[\s\S]{1,3}/g, cb_encode)
const _encode = (u: string) => btoa(utob(u))

export const encodeURI = (u: string) => base64Encode(u, true)
// decoder stuff
const re_btou = new RegExp([
  '[\xC0-\xDF][\x80-\xBF]',
  '[\xE0-\xEF][\x80-\xBF]{2}',
  '[\xF0-\xF7][\x80-\xBF]{3}'
].join('|'), 'g')

const cb_btou = function (c: string) {
  switch (c.length) {
    case 4: {
      const cp = ((0x07 & c.charCodeAt(0)) << 18) |
        ((0x3f & c.charCodeAt(1)) << 12) |
        ((0x3f & c.charCodeAt(2)) << 6) |
        (0x3f & c.charCodeAt(3))
      const offset = cp - 0x10000
      return (fromCharCode((offset >>> 10) + 0xD800) + fromCharCode((offset & 0x3FF) + 0xDC00))
    }
    case 3:
      return fromCharCode(
        ((0x0f & c.charCodeAt(0)) << 12) |
        ((0x3f & c.charCodeAt(1)) << 6) |
        (0x3f & c.charCodeAt(2))
      )
    default:
      return fromCharCode(
        ((0x1f & c.charCodeAt(0)) << 6) | (0x3f & c.charCodeAt(1))
      )
  }
}

const btou = (b: string) => b.replace(re_btou, cb_btou)
const cb_decode = function (c: string) {
  const len = c.length
  const padLen = len % 4
  const n = (len > 0 ? b64tab[c.charAt(0)] << 18 : 0) |
    (len > 1 ? b64tab[c.charAt(1)] << 12 : 0) |
    (len > 2 ? b64tab[c.charAt(2)] << 6 : 0) |
    (len > 3 ? b64tab[c.charAt(3)] : 0)
  const chars = [
    fromCharCode(n >>> 16),
    fromCharCode((n >>> 8) & 0xff),
    fromCharCode(n & 0xff)
  ]
  chars.length -= [0, 0, 2, 1][padLen]
  return chars.join('')
}

const atob = (a: string) => a.replace(/[\s\S]{1,4}/g, cb_decode)
const _decode = (a: string) => btou(atob(a))

export const base64Encode = function (u: string, uriSafe: boolean = false) {
  return !uriSafe
    ? _encode(String(u))
    : _encode(String(u)).replace(/[+\/]/g, function (m0) {
      return m0 === '+' ? '-' : '_'
    }).replace(/=/g, '')
}

export const base64Decode = function (a: string): string | null {
  try {
    return _decode(
      String(a).replace(/[-_]/g, function (m0) {
        return m0 === '-' ? '+' : '/'
      })
        .replace(/[^A-Za-z\d+\/]/g, '')
    )
  } catch (e) {
    return null
  }
}
